package com.android.server.alarm;

import static com.android.server.alarm.AlarmManagerService.DEBUG_BATCH;
import static com.android.server.alarm.AlarmManagerService.isTimeTickAlarm;

import android.app.AlarmManager;
import android.util.Log;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.function.Predicate;

public class AlignAlarmStore extends BatchingAlarmStore{
    private final ArrayList<AlignBatch> mAlarmBatches = new ArrayList<>();
    private int mSize;
    private HardwareCollector collector = new HardwareCollector();
    private Runnable mOnAlarmClockRemoved;

    private int mDeliveryNum = 0;
    private int mWakeupNum = 0;
    private long mAlignNum = 0;

    private static final Comparator<AlignBatch> sBatchOrder = Comparator.comparingLong(b -> b.mEnd);

    private static final Comparator<Alarm> sIncreasingTimeOrder = Comparator.comparingLong(
            Alarm::getWhenElapsed);

    public ArrayList<Alarm> remove(Predicate<Alarm> whichAlarms) {
        final ArrayList<Alarm> removed = new ArrayList<>();
        for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
            final AlignBatch b = mAlarmBatches.get(i);
            removed.addAll(b.remove(whichAlarms));
            if (b.size() == 0) {
                mAlarmBatches.remove(i);
            }
        }
        if (!removed.isEmpty()) {
            mSize -= removed.size();
            // Not needed if only whole batches were removed, but keeping existing behavior.
            rebatchAllAlarms();
        }
        return removed;
    }

    private void rebatchAllAlarms() {
        final long start = mStatLogger.getTime();
        final ArrayList<AlignBatch> oldBatches = (ArrayList<AlignBatch>) mAlarmBatches.clone();
        mAlarmBatches.clear();
        for (final AlignBatch batch : oldBatches) {
            for (int i = 0; i < batch.size(); i++) {
                insertAndBatchAlarm(batch.get(i));
            }
        }
        mStatLogger.logDurationStat(Stats.REBATCH_ALL_ALARMS, start);
    }

    private void insertAndBatchAlarm(Alarm alarm) {
        final int whichBatch = ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) ? -1
                : attemptCoalesce(alarm.getWhenElapsed(), alarm.getMaxWhenElapsed(), alarm.uid);

        if (whichBatch < 0) {
            addBatch(mAlarmBatches, new AlignBatch(alarm));
        } else {
            final AlignBatch batch = mAlarmBatches.get(whichBatch);
            if (batch.add(alarm)) {
                // The start time of this batch advanced, so batch ordering may
                // have just been broken.  Move it to where it now belongs.
                mAlarmBatches.remove(whichBatch);
                addBatch(mAlarmBatches, batch);
            }
        }
    }

    @Override
    public long getNextWakeupDeliveryTime() {
        for (AlignBatch b : mAlarmBatches) {
            if (b.hasWakeups()) {
                return b.mEnd;
            }
        }
        return 0;
    }

    @Override
    public long getNextDeliveryTime() {
        if (mAlarmBatches.size() > 0) {
            return mAlarmBatches.get(0).mStart;
        }
        return 0;
    }

    @Override
    public ArrayList<Alarm> removePendingAlarms(long nowElapsed) {
        final ArrayList<Alarm> removedAlarms = new ArrayList<>();
        while (mAlarmBatches.size() > 0) {
            final AlignBatch batch = mAlarmBatches.get(0);
            if (batch.mEnd > nowElapsed) {
                break;
            }
            mAlarmBatches.remove(0);
            for (int i = 0; i < batch.size(); i++) {
                removedAlarms.add(batch.get(i));
            }
            mDeliveryNum += 1;
            if (batch.hasWakeups()){
                mWakeupNum += 1;
            }
        }
        mSize -= removedAlarms.size();
        return removedAlarms;
    }

    static void addBatch(ArrayList<AlignBatch> list, AlignBatch newBatch) {
        int index = Collections.binarySearch(list, newBatch, sBatchOrder);
        if (index < 0) {
            index = 0 - index - 1;
        }
        list.add(index, newBatch);
    }

    // find suitable batch
    private int attemptCoalesce(long whenElapsed, long maxWhen, int uid) {
        final int n = mAlarmBatches.size();
        int priority;
        final ArrayList<Alarm> batchPriority = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            AlignBatch b = mAlarmBatches.get(i);
            if ((b.mFlags & AlarmManager.FLAG_STANDALONE) == 0 && b.canHold(whenElapsed, maxWhen)) {
                priority = getPriority(b,whenElapsed,maxWhen,uid);
            }
        }
        return -1;
    }

    // find suitable batch
    @Override
    public boolean setSuitableBatch(long completedJobTimeElapsed, int uid){
        int whichBatch;
        whichBatch = this.attemptCoalesce(completedJobTimeElapsed,completedJobTimeElapsed,uid);
        if(whichBatch < 0)
            return false;
        else{
            AlignBatch batch = this.mAlarmBatches.get(whichBatch);
            if(batch.setExactTime(completedJobTimeElapsed,uid)) {
                mAlarmBatches.remove(whichBatch);
                addBatch(mAlarmBatches, batch);
            }
        }
        return true;
    }



    // Return the priority of each batch
    private int getPriority(AlignBatch b, long whenElapsed, long maxWhen, int uid) {
        int priority = 0;
        float overlap;
        // config

        // overlap priority
        double[] overlap_thresholds = { 0.75, 0.5, 0.25, 0};
        double[] hardware_thresholds = {0.75, 0.5, 0.25, 0};
        overlap = getOverlap(b, whenElapsed, maxWhen);
        if (!b.hasWakeups()){
            int i = 0;
            for (double threshold : overlap_thresholds){
                if (overlap >= threshold){
                    priority += i;
                    break;
                }
                i = i + 4;
            }
        }
        else {
            int i = 4 * 4;
            for (double threshold : overlap_thresholds){
                if (overlap >= threshold){
                    priority += i;
                    break;
                }
                i = i + 4;
            }
        }
        // hardware priority
        int i = 0;
        double hardware_similarity = collector.getHardwareSimilarity(collector.getUidHardware(uid),b.hardwareSet);
        for (double threshold : overlap_thresholds){
            if (hardware_similarity >= threshold){
                priority += i;
                break;
            }
            i = i+1;
        }
        return priority;
    }

    // Return the overlap of alarm and batch
    private float getOverlap(AlignBatch b, long whenElapsed, long maxWhen){
        float overlap;
        if (whenElapsed != maxWhen){
            if (whenElapsed < b.mStart){
                if (maxWhen < b.mEnd)
                    overlap = (maxWhen - b.mStart) / (b.mEnd - b.mStart);
                else
                    overlap = (b.mEnd - b.mStart) / (b.mEnd - b.mStart);
            }
            else{
                if (maxWhen < b.mEnd)
                    overlap = (maxWhen - whenElapsed) / (b.mEnd - b.mStart);
                else
                    overlap = (b.mEnd - whenElapsed) / (b.mEnd - b.mStart);
            }
        }
        else
            overlap = 0;
        return overlap;
    }

    // Update the table of hardware and uid when device is charging
    public boolean updateTable(){
        try {
            collector.updateHardwareTable();
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    public long getDeliveryNum() {
        return mDeliveryNum;
    }

    public long getAlignNum(){
        return mAlignNum;
    }

    final class AlignBatch extends BatchingAlarmStore.Batch{
        ArrayList<String> hardwareSet;
        HardwareCollector collector;

        AlignBatch(Alarm seed) {
            super(seed);
            hardwareSet.addAll(collector.getUidHardware(seed.uid));
        }

        boolean add(Alarm alarm) {
            boolean newStart = false;
            // narrows the batch if necessary; presumes that canHold(alarm) is true
            int index = Collections.binarySearch(mAlarms, alarm, sIncreasingTimeOrder);
            if (index < 0) {
                index = 0 - index - 1;
            }
            mAlarms.add(index, alarm);
            if (DEBUG_BATCH) {
                Log.d(TAG, "Adding " + alarm + " to " + this);
            }
            if (alarm.getWhenElapsed() > mStart) {
                mStart = alarm.getWhenElapsed();
                newStart = true;
            }
            if (alarm.getMaxWhenElapsed() < mEnd) {
                mEnd = alarm.getMaxWhenElapsed();
            }
            mFlags |= alarm.flags;

            // add hardwareSet
            hardwareSet.addAll(collector.getUidHardware(alarm.uid));

            if (DEBUG_BATCH) {
                Log.d(TAG, "    => now " + this);
            }
            return newStart;
        }

        ArrayList<Alarm> remove(Predicate<Alarm> predicate) {
            final ArrayList<Alarm> removed = new ArrayList<>();
            long newStart = 0;  // recalculate endpoints as we go
            long newEnd = Long.MAX_VALUE;
            int newFlags = 0;
            ArrayList<String> newHardwareSet = new ArrayList<>();
            for (int i = 0; i < mAlarms.size(); ) {
                Alarm alarm = mAlarms.get(i);
                if (predicate.test(alarm)) {
                    removed.add(mAlarms.remove(i));
                    if (alarm.alarmClock != null && mOnAlarmClockRemoved != null) {
                        mOnAlarmClockRemoved.run();
                    }
                    if (isTimeTickAlarm(alarm)) {
                        // This code path is not invoked when delivering alarms, only when removing
                        // alarms due to the caller cancelling it or getting uninstalled, etc.
                        Log.d(TAG, "Removed TIME_TICK alarm");
                    }
                } else {
                    if (alarm.getWhenElapsed() > newStart) {
                        newStart = alarm.getWhenElapsed();
                    }
                    if (alarm.getMaxWhenElapsed() < newEnd) {
                        newEnd = alarm.getMaxWhenElapsed();
                    }
                    newFlags |= alarm.flags;
                    i++;
                    newHardwareSet.addAll(collector.getUidHardware(alarm.uid));
                }
            }
            if (!removed.isEmpty()) {
                // commit the new batch bounds
                mStart = newStart;
                mEnd = newEnd;
                mFlags = newFlags;
                hardwareSet = newHardwareSet;
            }
            return removed;
        }

        // Set the batch to start exactly
        public boolean setExactTime(long time,int uid){
            Log.d(TAG,"*** Setting exact time ***");
            if (this.mStart < time && this.mEnd > time){
                this.mStart = time;
                this.mEnd = time;
                this.hardwareSet.addAll(collector.getUidHardware(uid));
                return true;
            }
            return false;
        }
    }
}
